Skip to main content

Advance Types

Literal Types

In TypeScript, Literal Types allow you to specify exact values that a variable can hold, rather than just a general type like string or number.

  • A string type means any string ("hello", "world", "foo").
  • A string literal type means only one specific string (like "hello").

This is extremely useful when you want to restrict values to a limited set of possibilities.

let direction: "north" | "south" | "east" | "west";

direction = "north"; // ✅ allowed
direction = "south"; // ✅ allowed
direction = "up"; // ❌ Error: Type '"up"' is not assignable

Type Narrowing

In TypeScript, narrowing means reducing a broad type (like string | number) into a more specific type at runtime based on some checks.

function printLength(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is a string here
console.log(value.length);
} else {
// Here, value must be a number
console.log(value.toFixed(2));
}
}

Ways to Narrow Types

There are several techniques:

  • typeof checks
  • instanceof checks
  • Custom Type Guards (value is Type)

Narrowing with typeof

The typeof operator is used to check primitive types (string, number, boolean, bigint, symbol, undefined, and object).

function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value; // padding is number
} else {
return padding + value; // padding is string
}
}

console.log(padLeft("Hello", 4)); // " Hello"
console.log(padLeft("Hello", ">> ")); // ">> Hello"
  • typeof works great for primitives.
  • It won’t help for objects or classes — that’s where instanceof comes in.

Narrowing with instanceof

instanceof is used to check whether an object is created from a particular class or constructor function.

class Dog {
bark() {
console.log("Woof!");
}
}

class Cat {
meow() {
console.log("Meow!");
}
}

function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal is narrowed to Dog
} else {
animal.meow(); // animal is narrowed to Cat
}
}

makeSound(new Dog()); // Woof!
makeSound(new Cat()); // Meow!
  • Use instanceof when dealing with classes and objects.
  • It doesn’t work for primitives (use typeof instead).

Narrowing with Type Guards (Custom Functions)

Sometimes, you need your own checks for more complex types. A type guard is a function that returns a type predicate (param is Type).

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}

function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // animal is narrowed to Fish
} else {
animal.fly(); // animal is narrowed to Bird
}
}
  • isFish is a user-defined type guard.
  • The animal is Fish return type tells TypeScript that when this function returns true, the variable is of type Fish.

Mapped Types

A Mapped Type lets you create a new type by transforming each property of an existing type according to some rule.

Think of it as: Take a type → iterate over its keys → create a new type with modified properties.

type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
  • keyof Type → gets all property names of Type as a union.
  • Property in keyof Type → iterate over each key.
  • The mapped type assigns new property types.

Basic Mapped Types

type Features = {
darkMode: () => void;
multiLanguage: () => void;
};

type FeatureFlags = {
[K in keyof Features]: boolean;
};

// Equivalent to:
type FeatureFlagsManual = {
darkMode: boolean;
multiLanguage: boolean;
};

Every property of Features got transformed into boolean.

Built-in Mapped Types (Readonly, Partial, Required)

TypeScript provides some utility types built using mapped types.

ReadOnly

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

type User = {
id: number;
name: string;
};

type ReadonlyUser = Readonly<User>;

// Equivalent:
type ReadonlyUserManual = {
readonly id: number;
readonly name: string;
};

Partial

type Partial<T> = {
[P in keyof T]?: T[P];
};

type UserPartial = Partial<User>;

// Equivalent:
type UserPartialManual = {
id?: number;
name?: string;
};

All properties are now optional.

Required

type Required<T> = {
[P in keyof T]-?: T[P];
};

type UserOptional = {
id?: number;
name?: string;
};

type UserRequired = Required<UserOptional>;

// Equivalent:
type UserRequiredManual = {
id: number;
name: string;
};
  • The -? removes the optional modifier.
  • So all properties are required.

Remapping Keys

You can also remap keys using as.

type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Person = {
name: string;
age: number;
};

type PersonGetters = Getters<Person>;

// Equivalent:
type PersonGettersManual = {
getName: () => string;
getAge: () => number;
};
  • Each key is transformed into a new key (getName, getAge).
  • Each value becomes a function returning the original property type.

Conditional Types

A Conditional Type looks like this:

T extends U ? X : Y

Meaning:

  • If T can be assigned to U → return X
  • Otherwise → return Y

This is checked at compile-time, not at runtime.

Basic Conditional Type

type IsString<T> = T extends string ? "Yes" : "No";

type A = IsString<string>; // "Yes"
type B = IsString<number>; // "No"

You can use this to create type-level conditions.

Extracting Return Types

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = GetReturnType<() => number>; // number
type B = GetReturnType<(x: string) => boolean>; // boolean
type C = GetReturnType<string>; // never
  • infer R → captures the return type of the function.
  • If T is a function, return its return type. Otherwise → never. This is how TypeScript’s built-in ReturnType<T> works.

Conditional Type with Unions (Distributive Behavior)

Conditional types are distributive over unions.

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<number>; // number[]
type B = ToArray<number | string>; // number[] | string[]

When you pass number | string, TypeScript distributes: ToArray<number> | ToArray<string>number[] | string[].

This is powerful, but sometimes you don’t want distributive behavior → you can wrap in square brackets:

type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type C = ToArrayNonDistributive<number | string>; // (number | string)[]

Filtering Types

You can use conditional types to filter properties.

type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
  • ``"a"extends"a"→ becomesnever` → excluded.
  • ``"b"and"c"` remain.

Conditional Type with infer

The infer keyword lets you extract a type inside a conditional.

type FirstElement<T> = T extends [infer U, ...any[]] ? U : never;

type A = FirstElement<[string, number, boolean]>; // string
type B = FirstElement<[]>; // never
  • If T is a tuple, extract the first element type.
  • If not, return never.

Utility Types

Partial<T>

Makes all properties optional.

type User = {
id: number;
name: string;
age: number;
};

type PartialUser = Partial<User>;
// Equivalent:
type PartialUserManual = {
id?: number;
name?: string;
age?: number;
};

Required<T>

Makes all properties required (opposite of Partial).

type UserOptional = {
id?: number;
name?: string;
};

type UserRequired = Required<UserOptional>;
// Equivalent:
type UserRequiredManual = {
id: number;
name: string;
};

Readonly<T>

Makes all properties read-only.

type User = {
id: number;
name: string;
};

type ReadonlyUser = Readonly<User>;
// Equivalent:
type ReadonlyUserManual = {
readonly id: number;
readonly name: string;
};

const user: ReadonlyUser = { id: 1, name: "Alice" };
user.id = 2; // ❌ Error: Cannot assign to 'id'

Pick<T, K>

Creates a type by picking a subset of properties.

type User = {
id: number;
name: string;
age: number;
};

type UserPreview = Pick<User, "id" | "name">;
// Equivalent:
type UserPreviewManual = {
id: number;
name: string;
};

Omit<T, K>

Creates a type by removing specific properties.

type User = {
id: number;
name: string;
age: number;
};

type UserWithoutAge = Omit<User, "age">;
// Equivalent:
type UserWithoutAgeManual = {
id: number;
name: string;
};

Record<K, T>

Constructs a type with keys K and values T.

type Roles = "admin" | "editor" | "viewer";

type RolePermissions = Record<Roles, boolean>;
// Equivalent:
type RolePermissionsManual = {
admin: boolean;
editor: boolean;
viewer: boolean;
};

const permissions: RolePermissions = {
admin: true,
editor: false,
viewer: true,
};

Exclude<T, U>

Excludes types from a union.

type Letters = "a" | "b" | "c";

type WithoutA = Exclude<Letters, "a">;
// "b" | "c"

Extract<T, U>

Extracts types that are assignable to U.

type Letters = "a" | "b" | "c";

type OnlyA = Extract<Letters, "a" | "d">;
// "a"

Opposite of Exclude.